package plp.orientadaObjetos2.ambiente;

import java.util.HashMap;
import java.util.Set;
import java.util.Stack;

import plp.expressions2.memory.VariavelJaDeclaradaException;
import plp.expressions2.memory.VariavelNaoDeclaradaException;
import plp.orientadaObjetos1.comando.ChamadaProcedimento;
import plp.orientadaObjetos1.comando.Procedimento;
import plp.orientadaObjetos1.excecao.declaracao.ClasseJaDeclaradaException;
import plp.orientadaObjetos1.excecao.declaracao.ClasseNaoDeclaradaException;
import plp.orientadaObjetos1.excecao.declaracao.ObjetoJaDeclaradoException;
import plp.orientadaObjetos1.excecao.declaracao.ObjetoNaoDeclaradoException;
import plp.orientadaObjetos1.excecao.declaracao.ProcedimentoJaDeclaradoException;
import plp.orientadaObjetos1.excecao.declaracao.ProcedimentoNaoDeclaradoException;
import plp.orientadaObjetos1.excecao.execucao.EntradaInvalidaException;
import plp.orientadaObjetos1.expressao.leftExpression.Id;
import plp.orientadaObjetos1.expressao.valor.Valor;
import plp.orientadaObjetos1.expressao.valor.ValorRef;
import plp.orientadaObjetos1.expressao.valor.ValorString;
import plp.orientadaObjetos1.memoria.ContextoObjeto;
import plp.orientadaObjetos1.memoria.Objeto;
import plp.orientadaObjetos1.memoria.colecao.ListaValor;
import plp.orientadaObjetos1.util.Tipo;
import plp.orientadaObjetos2.memoria.AmbienteExecucaoOO2;
import plp.orientadaObjetos2.memoria.ContextoExecucaoOO2;
import plp.orientadaObjetos2.memoria.DefClasseOO2;
import plp.orientadaObjetos2.memoria.ObjetoNativo;

public class Classe {

	private String nome;
	private Id idClasse;

	public Classe(String nome) {
		this.nome = nome;
		this.idClasse = new Id(this.nome);
	}

	public ValorRef instancia(AmbienteExecucaoOO2 ambiente, ListaValor parametrosAvaliados){
		ValorRef ref = null;
		try {
			DefClasseOO2 defClasse = (DefClasseOO2) ambiente.getDefClasse(this.idClasse);
			
			AmbienteExecucaoOO2 ambienteClasse = (AmbienteExecucaoOO2) defClasse.getDecVariavel().elabora(new ContextoExecucaoOO2(ambiente));
			ContextoObjeto estado = new ContextoObjeto(ambienteClasse.getPilha().pop());
			Objeto objeto = new Objeto(defClasse.getIdClasse(), estado);
			iniciarEstadoObjeto(objeto, ambienteClasse);
			ref = ambiente.getProxRef();
			ambiente.mapObjeto(ref, objeto);
			
			Procedimento metodo = defClasse.getConstrutor().getProcedimento();
			if(metodo.getParametrosFormais().length() != parametrosAvaliados.length()){
				throw new ExcecaoNativaException("Construtor necessita de " +
													metodo.getParametrosFormais().length() +
													" parametro(s), mas recebeu " +
													parametrosAvaliados.length() + " parametro(s)");
			}
			AmbienteExecucaoOO2 ambienteProcedimento = new ContextoExecucaoOO2(ambiente);

			ambienteProcedimento.changeValor(new Id("this"), ref);

			new ChamadaProcedimento(metodo, null, parametrosAvaliados).executar(ambienteProcedimento);
			
			
		} catch (ClasseNaoDeclaradaException e) {
			throw new ExcecaoNativaException(e);
		} catch (ObjetoJaDeclaradoException e) {
			throw new ExcecaoNativaException(e);
		} catch (VariavelNaoDeclaradaException e) {
			throw new ExcecaoNativaException(e);
		} catch (VariavelJaDeclaradaException e) {
			throw new ExcecaoNativaException(e);
		} catch (ObjetoNaoDeclaradoException e) {
			throw new ExcecaoNativaException(e);
		} catch (ClasseJaDeclaradaException e) {
			throw new ExcecaoNativaException(e);
		} catch (ProcedimentoNaoDeclaradoException e) {
			throw new ExcecaoNativaException(e);
		} catch (ProcedimentoJaDeclaradoException e) {
			throw new ExcecaoNativaException(e);
		} catch (EntradaInvalidaException e) {
			throw new ExcecaoNativaException(e);
		}
		return  ref;
	}

	private void iniciarEstadoObjeto(Objeto objeto, AmbienteExecucaoOO2 ambiente)
				throws ClasseNaoDeclaradaException, VariavelNaoDeclaradaException,
						VariavelJaDeclaradaException, ObjetoNaoDeclaradoException,
						ClasseJaDeclaradaException, ObjetoJaDeclaradoException{
		DefClasseOO2 defClasse = (DefClasseOO2) ambiente.getDefClasse(objeto.getClasse());
		Stack<DefClasseOO2> pilhaHeranca = new Stack<DefClasseOO2>();
		while(defClasse.getNomeSuperClasse() != null && ambiente.getDefClasse(defClasse.getNomeSuperClasse()) != null){
			defClasse = (DefClasseOO2) ambiente.getDefClasse(defClasse.getNomeSuperClasse());
			pilhaHeranca.push(defClasse);
		}
		while(!pilhaHeranca.isEmpty()){
			atualizaEstadoObjeto(objeto, ambiente, pilhaHeranca.pop());
		}
		
	}

	private void atualizaEstadoObjeto(Objeto objeto,
			AmbienteExecucaoOO2 ambiente, DefClasseOO2 defClasse)
			throws ObjetoNaoDeclaradoException, ClasseNaoDeclaradaException,
			ClasseJaDeclaradaException, ObjetoJaDeclaradoException {
		AmbienteExecucaoOO2 ambienteClasse = (AmbienteExecucaoOO2) defClasse.getDecVariavel().elabora(new ContextoExecucaoOO2(ambiente));
		HashMap<plp.expressions2.expression.Id, Valor> mapeamentoIdValor = ambienteClasse.getPilha().pop();
		Set<plp.expressions2.expression.Id> keySet = mapeamentoIdValor.keySet();
		for (plp.expressions2.expression.Id id : keySet) {
			if(!objeto.getEstado().containsKey(id)){
				objeto.getEstado().put(id, mapeamentoIdValor.get(id));
			}
		}
	}

	public ValorRef getAtributo(AmbienteExecucaoOO2 ambiente, ListaValor argumentos){
		ValorRef proxRef;
		try {
			ValorString valorString = (ValorString) argumentos.getHead();
			String nomeClasse = valorString.valor();
			Tipo tipo = getTipoAtributo(ambiente, nomeClasse);
			proxRef = ambiente.getProxRef();
			Atributo atributo = new Atributo(nomeClasse, tipo.getTipo().getIdName());
			ambiente.mapObjeto(proxRef, new ObjetoNativo(new Id(Nomes.CLASSE_ATRIBUTO), atributo));
		} catch (ClasseNaoDeclaradaException e) {
			throw new ExcecaoNativaException(e);
		} catch (ObjetoJaDeclaradoException e) {
			throw new ExcecaoNativaException(e);
		}
		return proxRef;
	}

	private Tipo getTipoAtributo(AmbienteExecucaoOO2 ambiente, String nomeClasse)
			throws ClasseNaoDeclaradaException {
		DefClasseOO2 defClasse = (DefClasseOO2) ambiente.getDefClasse(this.idClasse);
		Id idAtributo = new Id(nomeClasse);
		try{
			Tipo tipo = defClasse.getTipoAtributo(idAtributo);
			return tipo;
		} catch(VariavelNaoDeclaradaException vnde){
			if(defClasse.getNomeSuperClasse() != null){
				return ambiente.getDefClasse(defClasse.getNomeSuperClasse()).getTipoAtributo(idAtributo);
			} else {
				throw vnde;
			}
		}
	}

	public ValorRef getMetodo(AmbienteExecucaoOO2 ambiente, ListaValor argumentos){
		ValorRef proxRef = null;
		try {
			ValorString valorString = (ValorString) argumentos.getHead();
			String nomeMetodo = valorString.valor();
			proxRef = ambiente.getProxRef();
			Metodo metodo = new Metodo(new Id(nomeMetodo), this);
			ambiente.mapObjeto(proxRef, new ObjetoNativo(new Id(Nomes.CLASSE_METODO), metodo));
		} catch (ObjetoJaDeclaradoException e) {
			throw new ExcecaoNativaException(e);
		}
		return proxRef;
	}

	public String getNome() {
		return nome;
	}

	public Id getIdClasse() {
		return idClasse;
	}

}
